﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="ColorHatTimeTrackerPackage.cs" company="Mar3ek">
//   http://mar3ek.wordpress.com
// </copyright>
// <summary>
//   This is the class that implements the package exposed by this assembly.
//   The minimum requirement for a class to be considered a valid package for Visual Studio
//   is to implement the IVsPackage interface and register itself with the shell.
//   This package uses the helper classes defined inside the Managed Package Framework (MPF)
//   to do it: it derives from the Package class that provides the implementation of the
//   IVsPackage interface and uses the registration attributes defined in the framework to
//   register itself and its components with the shell.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace Mar3ek.ColorHatTimeTracker
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Design;
    using System.Diagnostics;
    using System.Linq;
    using System.Runtime.InteropServices;
    using System.Threading;
    using System.Threading.Tasks;
    using System.Windows;

    using ColorhatNet;

    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Shell;
    using Microsoft.VisualStudio.Shell.Interop;

    using Task = System.Threading.Tasks.Task;

    /// <summary>
    /// This is the class that implements the package exposed by this assembly.
    /// The minimum requirement for a class to be considered a valid package for Visual Studio
    /// is to implement the IVsPackage interface and register itself with the shell.
    /// This package uses the helper classes defined inside the Managed Package Framework (MPF)
    /// to do it: it derives from the Package class that provides the implementation of the 
    /// IVsPackage interface and uses the registration attributes defined in the framework to 
    /// register itself and its components with the shell.
    /// </summary>
    [PackageRegistration(UseManagedResourcesOnly = true)]
    [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
    [ProvideMenuResource("Menus.ctmenu", 1)]
    [Guid(GuidList.GuidColorHatTimeTrackerPkgString)]
    [ProvideOptionPage(typeof(OptionsPage), "Colorhat", "Login details", 0, 0, true)]
    public sealed class ColorHatTimeTrackerPackage : Package, IDisposable
    {
        /// <summary>
        /// The timer for updating UI.
        /// </summary>
        private readonly Timer uiTimer;

        /// <summary>
        /// The mutex for protecting access to the API login.
        /// </summary>
        private readonly Mutex mutex = new Mutex();

        /// <summary>
        /// Contains an empty string used in combo boxes
        /// </summary>
        private readonly List<string> emptyStringList = new List<string> { string.Empty };

        /// <summary>
        /// The ColorHat API implementation.
        /// </summary>
        private readonly ColorhatApi api = new ColorhatApi();

        /// <summary>
        /// Service selection combo command
        /// </summary>
        private OleMenuCommand serviceCombo;

        /// <summary>
        /// Project combo selection command
        /// </summary>
        private OleMenuCommand projectCombo;

        /// <summary>
        /// Project combo selection command
        /// </summary>
        private OleMenuCommand projectComboListCommand;

        /// <summary>
        /// Service combo selection command
        /// </summary>
        private OleMenuCommand serviceComboListCommand;

        /// <summary>
        /// Refresh command
        /// </summary>
        private OleMenuCommand refreshCommand;

        /// <summary>
        /// Command for starting the timing operation.
        /// </summary>
        private OleMenuCommand toggleTimingCommand;

        /// <summary>
        /// The selected client.
        /// </summary>
        private string selectedProject = string.Empty;

        /// <summary>
        /// The selected service.
        /// </summary>
        private string selectedService = string.Empty;

        /// <summary>
        /// The start time of the currently tracked time segment.
        /// </summary>
        private DateTime? timingStart;

        /// <summary>
        /// indicates whether a time segment is currently being tracked.
        /// </summary>
        private bool isTiming;

        /// <summary>
        /// Indicates whether the API is in the login process
        /// </summary>
        private bool isLoading;

        /// <summary>
        /// The project currently being tracked.
        /// </summary>
        private string timingSegmentProject;

        /// <summary>
        /// The service currently being tracked.
        /// </summary>
        private string timingSegmentService;

        /// <summary>
        /// Indicates whether this instance has been disposed.
        /// </summary>
        private bool isDisposed;

        /// <summary>
        /// Initializes a new instance of the <see cref="ColorHatTimeTrackerPackage"/> class. 
        /// Default constructor of the package.
        /// Inside this method you can place any initialization code that does not require 
        /// any Visual Studio service because at this point the package object is created but 
        /// not sited yet inside Visual Studio environment. The place to do all the other 
        /// initialization is the Initialize method.
        /// </summary>
        public ColorHatTimeTrackerPackage()
        {
            Debug.WriteLine("Entering constructor for: {0}", this);
            this.uiTimer = new Timer(this.UiTimerCallback);
        }

        /// <summary>
        /// Releases the resources held by this instance.
        /// </summary>
        public void Dispose()
        {
            if (this.isDisposed)
            {
                return;
            }

            this.Dispose(true);
        }

        /// <summary>
        ///  Releases the resources held by this instance.
        /// </summary>
        /// <param name="disposing">
        /// Whether this instance is being disposed.
        /// </param>
        protected override void Dispose(bool disposing)
        {
            base.Dispose(disposing);
            if (disposing)
            {
                this.uiTimer.Dispose();
                this.mutex.Dispose();
            }

            this.isDisposed = true;
        }

        /// <summary>
        /// Initialization of the package; this method is called right after the package is sited, so this is the place
        /// where you can put all the initialization code that rely on services provided by VisualStudio.
        /// </summary>
        protected override void Initialize()
        {
            Debug.WriteLine("Entering Initialize() of: {0}", this);
            base.Initialize();

            // Add our command handlers for menu (commands must exist in the .vsct file)
            var mcs = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;
            if (null == mcs)
            {
                return;
            }

            // Create the command for the menu item.
            var menuCommandId = new CommandID(GuidList.GuidColorHatTimeTrackerCmdSet, (int)PkgCmdIdList.CmdidColorhatToolbar);
            this.toggleTimingCommand = new OleMenuCommand(this.ToggleTimingCallback, menuCommandId) { Enabled = false };
            mcs.AddCommand(this.toggleTimingCommand);

            // command for the client combo itself
            var projectComboId = new CommandID(GuidList.GuidColorHatTimeTrackerCmdSet, (int)PkgCmdIdList.CmdidColorhatClients);
            this.projectCombo = new OleMenuCommand(this.ProjectComboSelectionCallback, projectComboId) { Enabled = false };
            mcs.AddCommand(this.projectCombo);

            // command to fill the client combo list
            var clientComboListId = new CommandID(GuidList.GuidColorHatTimeTrackerCmdSet, (int)PkgCmdIdList.ClientComboList);
            this.projectComboListCommand = new OleMenuCommand(this.ProjectComboListCallback, clientComboListId) { Enabled = false };
            mcs.AddCommand(this.projectComboListCommand);

            // command for the service combo itself
            var serviceComboId = new CommandID(GuidList.GuidColorHatTimeTrackerCmdSet, (int)PkgCmdIdList.CmdidColorhatServices);
            this.serviceCombo = new OleMenuCommand(this.ServiceComboSelectionCallback, serviceComboId) { Enabled = false };
            mcs.AddCommand(this.serviceCombo);

            // command to fill the service combo list
            var serviceComboListId = new CommandID(GuidList.GuidColorHatTimeTrackerCmdSet, (int)PkgCmdIdList.ServiceComboList);
            this.serviceComboListCommand = new OleMenuCommand(this.ServiceComboListCallback, serviceComboListId) { Enabled = false };
            mcs.AddCommand(this.serviceComboListCommand);

            // refresh command
            var refreshId = new CommandID(GuidList.GuidColorHatTimeTrackerCmdSet, (int)PkgCmdIdList.CmdidColorhatRefresh);
            this.refreshCommand = new OleMenuCommand(this.RefreshCallback, refreshId);
            mcs.AddCommand(this.refreshCommand);

            WriteToOutputPane("ColorHat Time Tracker - Startup successful\n");
            WriteToOutputPane(string.Format("\tRelease name: {0}", ReleaseInfo.ReleaseCodeName));
            WriteToOutputPane(string.Format("\t\"{0}\"", ReleaseInfo.ReleaseAdditionInfo));
            WriteToOutputPane(string.Format("\tVersion: {0}", ReleaseInfo.Version));
            WriteToOutputPane(string.Empty);
        }

        /// <summary>
        /// Queries whether is is possible to quit.
        /// </summary>
        /// <param name="canClose">
        /// Describes whether it is possible to quit.
        /// </param>
        /// <returns>
        /// The <see cref="int"/>.
        /// </returns>
        protected override int QueryClose(out bool canClose)
        {           
            if (this.isTiming)
            {
                var result = MessageBox.Show("Timing is in progress. Closing the IDE will cause the timing to stop. Do you want to upload this time segment before exiting?", "Colorhat Time Tracker", MessageBoxButton.YesNo, MessageBoxImage.Question);

                if (result == MessageBoxResult.Yes)
                {
                    Task.Run(async () => { await this.StopTiming(); }).Wait();
                }
            }

            this.uiTimer.Dispose();
            canClose = true;
            return VSConstants.S_OK;
        }

        /// <summary>
        /// Writes the specified message to the ColorHat output pane.
        /// </summary>
        /// <param name="message">
        /// The message to be written.
        /// </param>
        private static void WriteToOutputPane(string message)
        {
            var outputWindow = GetGlobalService(typeof(SVsOutputWindow)) as IVsOutputWindow;
            var paneGuid = new Guid(GuidList.GuidColorhatOutputPane);
            IVsOutputWindowPane pane;

            Trace.Assert(outputWindow != null, "outputWindow != null");

            var colorHatPaneOpened = outputWindow.GetPane(paneGuid, out pane);
            if (colorHatPaneOpened != VSConstants.S_OK)
            {
                var creationStatus = outputWindow.CreatePane(paneGuid, "ColorHat", 1, 0);
                if (creationStatus != VSConstants.S_OK)
                {
                    Trace.WriteLine("Unable to create the output pane.");
                    return;
                }

                outputWindow.GetPane(paneGuid, out pane);
            }

            pane.OutputString(message + "\n");
        }

        /// <summary>
        /// Transforms an exception into a programmer-readable message.
        /// </summary>
        /// <param name="exception">
        /// The exception.
        /// </param>
        /// <returns>
        /// The programmer-readable string.
        /// </returns>
        private static string ExceptionToMessage(Exception exception)
        {
            var baseException = exception.GetBaseException();
            return string.Format("An exception has occured in the ColorHat API:\n\tMessage: {0}\n\n\tStack trace: {1}", baseException.Message, baseException.StackTrace);
        }

        /// <summary>
        /// The toggle timing callback.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The event arguments.
        /// </param>
        private async void ToggleTimingCallback(object sender, EventArgs e)
        {
            if (!this.isTiming)
            {
                await this.StartTiming();
            }
            else
            {
                await this.StopTiming();
            }
        }

        /// <summary>
        /// Stops timing.
        /// </summary>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        private async Task StopTiming()
        {
            var now = DateTime.Now;
            this.isTiming = false;
            this.toggleTimingCommand.Checked = false;
            this.uiTimer.Change(Timeout.Infinite, -1);
            WriteToOutputPane(string.Format("Tracking stopped at {0}", now.ToString("HH:mm:ss")));

            var page = (OptionsPage)GetDialogPage(typeof(OptionsPage));
            var annotation = string.Empty;

            Trace.Assert(this.timingStart.HasValue, "Invalid timing start time.");

            var difference = now.Subtract(this.timingStart.Value);

            if (difference.TotalSeconds < 60)
            {
                WriteToOutputPane("The tracked period is too short for ColorHat to accept. It will be discarded.");
                this.ResetTiming();
                return;
            }

            if (!page.SuppressAnnotation)
            {
                ThreadHelper.Generic.Invoke(() =>
                    {
                        var w = new AnnotationWindow();
                        var vm = (AnnotationViewModel)w.DataContext;

                        vm.SegmentStart = this.timingStart.Value;

                        vm.SegmentEnd = now;

                        vm.Duration = difference.ToString("hh\\:mm");

                        vm.Client = this.selectedProject;
                        vm.Service = this.selectedService;

                        var result = w.ShowDialog();

                        if (result != true)
                        {
                            return;
                        }

                        annotation = vm.Annotation;
                    });
            }
            
            WriteToOutputPane("Uploading time segment data...");

            try
            {
                var uploadResult = await this.api.UploadTimeSegment(this.timingStart.Value, now, annotation, this.GetObjectId(this.timingSegmentProject, ObjectType.Project), this.GetObjectId(this.timingSegmentService, ObjectType.Service));
                WriteToOutputPane(uploadResult ? "Upload successful." : "Upload unsuccessful.");
            }
            catch (Exception ex)
            {
                WriteToOutputPane(ExceptionToMessage(ex));
                MessageBox.Show("An exception has occured in the ColorHat API. Details have been logged to the ColorHat output pane. If this problem persists, contact the extension manufacturer.", "ColorHat Time Tracker", MessageBoxButton.OK, MessageBoxImage.Warning);
            }
            
            this.ResetTiming();
        }

        /// <summary>
        /// Starts timing
        /// </summary>
        /// <returns>
        /// The <see cref="Task"/>.
        /// </returns>
        private async Task StartTiming()
        {
            if (this.isLoading)
            {
                MessageBox.Show("ColorHat API is loading. Stand by, please...", "ColorHat Time Tracker", MessageBoxButton.OK, MessageBoxImage.Information);
                return;
            }

            var now = DateTime.Now;
            if (!await this.VerifyCredentials(false))
            {
                return;
            }

            if (!this.isLoading)
            {
                this.timingSegmentProject = this.selectedProject;
                this.timingSegmentService = this.selectedService;
            }
            
            this.isTiming = true;
            this.timingStart = now;

            this.toggleTimingCommand.Checked = true;
            this.uiTimer.Change(0, 1000);
            WriteToOutputPane(
                string.Format(
                    "Tracking started at {0}\n\tProject: {1}\n\tService: {2}", 
                    this.timingStart.Value.ToString("HH:mm:ss"),
                    !string.IsNullOrEmpty(this.timingSegmentProject) ? this.timingSegmentProject : "-",
                    !string.IsNullOrEmpty(this.timingSegmentService) ? this.timingSegmentService : "-"));
        }

        /// <summary>
        /// Resets the timing variables.
        /// </summary>
        private void ResetTiming()
        {
            this.toggleTimingCommand.Text = "00:00:00";
            this.timingStart = null;
            this.timingSegmentProject = string.Empty;
            this.timingSegmentService = string.Empty;
        }

        /// <summary>
        /// Gets the object ID of the object with the specified name.
        /// </summary>
        /// <param name="objectName">
        /// The object name.
        /// </param>
        /// <param name="type">
        /// The type of the object.
        /// </param>
        /// <returns>
        /// Object ID if such an object exists in the API, otherwise null.
        /// </returns>
        private uint? GetObjectId(string objectName, ObjectType type)
        {
            switch (type)
            {
                case ObjectType.Project:
                    if (string.IsNullOrEmpty(objectName))
                    {
                        return null;
                    }

                    if (this.api.Projects.Values.Contains(objectName))
                    {
                        return this.api.Projects.First(p => p.Value == objectName).Key;
                    }

                    return null;
                case ObjectType.Service:
                    if (string.IsNullOrEmpty(objectName))
                    {
                        return null;
                    }

                    if (this.api.Services.Values.Contains(objectName))
                    {
                        return this.api.Services.First(p => p.Value == objectName).Key;
                    }

                    return null;
                case ObjectType.Customer:
                    if (string.IsNullOrEmpty(objectName))
                    {
                        return null;
                    }

                    if (this.api.Customers.Values.Contains(objectName))
                    {
                        return this.api.Customers.First(p => p.Value == objectName).Key;
                    }

                    return null;
                default:
                    Debug.Assert(false, "Unknown object type");
// ReSharper disable HeuristicUnreachableCode
                    return null;
// ReSharper restore HeuristicUnreachableCode
            }
        }

        /// <summary>
        /// Verifies the ColorHat credentials.
        /// </summary>
        /// <param name="silent">
        /// Whether to suppress message boxes.
        /// </param>
        /// <returns>
        /// True if credentials are valid, otherwise false.
        /// </returns>
        private async Task<bool> VerifyCredentials(bool silent)
        {
            bool result;
            
            var page = (OptionsPage)GetDialogPage(typeof(OptionsPage));

            if (string.IsNullOrEmpty(page.Username))
            {
                if (!silent)
                {
                    MessageBox.Show("Colorhat username is empty. You must specify your Colorhat login details in order to use this addin.\nYou can specify your login details in VS options.", "Colorhat credentials", MessageBoxButton.OK, MessageBoxImage.Warning);
                }

                return false;
            }

            if (string.IsNullOrEmpty(page.Password))
            {
                if (!silent)
                {
                    MessageBox.Show("Colorhat password is empty. You must specify your Colorhat login details in order to use this addin.\nYou can specify your login details in VS options.", "Colorhat credentials", MessageBoxButton.OK, MessageBoxImage.Warning);
                }

                return false;
            }

            if (this.api.IsLoggedIn)
            {
                return true;
            }

            try
            {
                this.mutex.WaitOne();
                this.isLoading = true;
                result = await this.api.LogOn(page.Username, page.Password);

                if (result)
                {
                    this.serviceCombo.Enabled = true;
                    this.projectCombo.Enabled = true;
                    this.projectComboListCommand.Enabled = true;
                    this.serviceComboListCommand.Enabled = true;
                    this.refreshCommand.Enabled = true;
                    this.toggleTimingCommand.Enabled = true;
                    WriteToOutputPane("Login successful, projects and services loaded");
                }
                else
                {
                    WriteToOutputPane("Login failed. Verify that your username and password are correct and try again");
                }
            }
            catch (Exception ex)
            {
                result = false;
                WriteToOutputPane(ExceptionToMessage(ex));
                MessageBox.Show("An exception has occured in the ColorHat API. Details have been logged to the ColorHat output pane. If this problem persists, contact the extension manufacturer.", "ColorHat Time Tracker", MessageBoxButton.OK, MessageBoxImage.Warning);
            }
            finally
            {
                this.isLoading = false;
                this.mutex.ReleaseMutex();
            }

            return result;
        }

        /// <summary>
        /// The UI timer callback.
        /// </summary>
        /// <param name="state">
        /// The state.
        /// </param>
        private void UiTimerCallback(object state)
        {
            if (null == this.toggleTimingCommand)
            {
                return;
            }

            if (!this.timingStart.HasValue)
            {
                return;
            }

            var difference = DateTime.Now.Subtract(this.timingStart.Value);
            this.toggleTimingCommand.Text = difference.ToString("hh\\:mm\\:ss");
        }

        /// <summary>
        /// The service combo selection callback.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The event arguments.
        /// </param>
        private async void ServiceComboSelectionCallback(object sender, EventArgs e)
        {
            if (this.isLoading)
            {
                return;
            }

            var eventArgs = e as OleMenuCmdEventArgs;

            if (eventArgs == null)
            {
                return;
            }

            var newChoice = eventArgs.InValue as string;
            var outValue = eventArgs.OutValue;

            Trace.Assert(outValue == IntPtr.Zero || newChoice == null, "Both outValue and newChoice are non-null.");

            if (outValue != IntPtr.Zero)
            {
                Marshal.GetNativeVariantForObject(this.selectedService, outValue);
            }
            else if (newChoice != null)
            {
                if (this.api.Services.Values.Contains(newChoice) || string.IsNullOrEmpty(newChoice))
                {
                    this.selectedService = newChoice;
                }
                else
                {
                    this.selectedService = newChoice;

                    // new service name
                    var response = MessageBox.Show(string.Format("The service '{0}' was not found in your Colorhat account. Do you wish to create it now?", newChoice), "Colorhat Time Tracker", MessageBoxButton.YesNo, MessageBoxImage.Question);

                    if (response != MessageBoxResult.Yes)
                    {
                        return;
                    }

                    await this.CreateNewService(newChoice);
                }
            }
            else
            {
                Trace.Assert(false, "Invalid execution path in ServiceComboSelectionCallback.");
            }
        }

        /// <summary>
        /// The service combo list callback.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The event arguments.
        /// </param>
        private async void ServiceComboListCallback(object sender, EventArgs e)
        {
            var eventArgs = e as OleMenuCmdEventArgs;
            if (eventArgs == null)
            {
                return;
            }

            var output = eventArgs.OutValue;

            if (this.isLoading)
            {
                Marshal.GetNativeVariantForObject(new List<string> { "Loading..." }.ToArray(), output);
                return;
            }

            if (!await this.VerifyCredentials(false))
            {
                return;
            }

            // return the list of available services + an empty string for empty selection
            Marshal.GetNativeVariantForObject(this.emptyStringList.Union(this.api.Services.Select(p => p.Value)).ToArray(), output);
        }

        /// <summary>
        /// The client combo selection callback.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The event arguments.
        /// </param>
        private async void ProjectComboSelectionCallback(object sender, EventArgs e)
        {
            if (this.isLoading)
            {
                return;
            }

            var eventArgs = e as OleMenuCmdEventArgs;

            if (eventArgs == null)
            {
                return;
            }

            var newChoice = eventArgs.InValue as string;
            var outValue = eventArgs.OutValue;

            Trace.Assert(outValue == IntPtr.Zero || newChoice == null, "Both outValue and newChoice are non-null.");

            if (outValue != IntPtr.Zero)
            {
                // the IDE is asking for a value to display
                Marshal.GetNativeVariantForObject(this.selectedProject, outValue);
            }
            else if (newChoice != null)
            {
                // the IDE is signalling a new value being selected
                if (this.api.Projects.Values.Contains(newChoice) || string.IsNullOrEmpty(newChoice))
                {
                    this.selectedProject = newChoice;
                }
                else
                {
                    this.selectedProject = newChoice;

                    // new project name
                    var response = MessageBox.Show(string.Format("The project '{0}' was not found in your Colorhat account. Do you wish to create it now?", newChoice), "Colorhat Time Tracker", MessageBoxButton.YesNo, MessageBoxImage.Question);

                    if (response != MessageBoxResult.Yes)
                    {
                        return;
                    }

                    await this.CreateNewProject(newChoice);
                }
            }
            else
            {
                Trace.Assert(false, "Invalid execution path in ProjectComvoSelectionCallback.");
            }
        }

        /// <summary>
        /// The project combo list callback.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The event arguments.
        /// </param>
        private async void ProjectComboListCallback(object sender, EventArgs e)
        {
            var eventArgs = e as OleMenuCmdEventArgs;

            if (eventArgs == null)
            {
                return;
            }

            var output = eventArgs.OutValue;

            if (this.isLoading)
            {
                Marshal.GetNativeVariantForObject(new List<string> { "Loading..." }.ToArray(), output);
                return;
            }

            if (!await this.VerifyCredentials(false))
            {
                return;
            }

            // return the list of available projects + an empty string for empty selection
            Marshal.GetNativeVariantForObject(this.emptyStringList.Union(this.api.Projects.Select(p => p.Value)).ToArray(), output);
        }

        /// <summary>
        /// The refresh callback.
        /// </summary>
        /// <param name="sender">
        /// The sender.
        /// </param>
        /// <param name="e">
        /// The event arguments.
        /// </param>
        private async void RefreshCallback(object sender, EventArgs e)
        {
            if (this.isLoading)
            {
                return;
            }

            if (!await this.VerifyCredentials(false))
            {
                return;
            }

            WriteToOutputPane("Refreshing project and service data...");

            try
            {
                await this.api.Refresh();
                WriteToOutputPane("Refresh successful");
            }
            catch (Exception ex)
            {
                WriteToOutputPane(ExceptionToMessage(ex));
                MessageBox.Show("An exception has occured in the ColorHat API. Details have been logged to the ColorHat output pane. If this problem persists, contact the extension manufacturer.", "ColorHat Time Tracker", MessageBoxButton.OK, MessageBoxImage.Warning);
            }
        }

        /// <summary>
        /// Creates a new ColorHat project
        /// </summary>
        /// <param name="projectName">
        /// The name of the new project.
        /// </param>
        /// <returns>
        /// Nothing - void.
        /// </returns>
        private async Task CreateNewProject(string projectName)
        {
            // we need to establish a client for the new project
            var w = new NewProjectWindow();
            var vm = (NewProjectViewModel)w.DataContext;

            // initialize the window's viewmodel
            vm.Initialize(this.api, projectName);

            var result = w.ShowDialog();

            if (result != true)
            {
                return;
            }

            WriteToOutputPane("Creating a new project: " + projectName);

            try
            {
                var customerId = this.GetObjectId(vm.SelectedCustomer, ObjectType.Customer);

                if (!customerId.HasValue)
                {
                    WriteToOutputPane(string.Format("The customer '{0}' was not found in your Colorhat account", vm.SelectedCustomer));
                    return;
                }

                var serviceCreated = await this.api.CreateProject(customerId.Value, null, projectName);

                if (serviceCreated)
                {
                    WriteToOutputPane("Project creation successful");
                }
                else
                {
                    this.selectedService = string.Empty;
                    WriteToOutputPane("Project creation failed");
                }
            }
            catch (Exception ex)
            {
                WriteToOutputPane(ExceptionToMessage(ex));
                MessageBox.Show("An exception has occured in the ColorHat API. Details have been logged to the ColorHat output pane. If this problem persists, contact the extension manufacturer.", "ColorHat Time Tracker", MessageBoxButton.OK, MessageBoxImage.Warning);
            }
        }

        /// <summary>
        /// Creates a new ColorHat service
        /// </summary>
        /// <param name="serviceName">
        /// The name of the new service.
        /// </param>
        /// <returns>
        /// Nothing - void.
        /// </returns>
        private async Task CreateNewService(string serviceName)
        {
            WriteToOutputPane("Creating a new service: " + serviceName);

            try
            {
                var serviceCreated = await this.api.CreateService(null, serviceName);

                if (serviceCreated)
                {
                    WriteToOutputPane("Service creation successful");
                }
                else
                {
                    this.selectedService = string.Empty;
                    WriteToOutputPane("Service creation failed");
                }
            }
            catch (Exception ex)
            {
                WriteToOutputPane(ExceptionToMessage(ex));
                MessageBox.Show("An exception has occured in the ColorHat API. Details have been logged to the ColorHat output pane. If this problem persists, contact the extension manufacturer.", "ColorHat Time Tracker", MessageBoxButton.OK, MessageBoxImage.Warning);
            }
        }
    }
}
